// Copyright 2018 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file

#include <errno.h>
#include <fcntl.h>
#include <getopt.h>
#include <string.h>

#include <fbl/string_printf.h>
#include <fbl/unique_fd.h>
#include <fbl/unique_ptr.h>
#include <fs-test-utils/perftest.h>
#include <fuchsia/hardware/block/c/fidl.h>
#include <lib/fzl/fdio.h>
#include <lib/fzl/time.h>
#include <lib/zx/time.h>
#include <zircon/assert.h>

namespace fs_test_utils {
namespace {

constexpr char kUsage[] = R"(
Usage:

    %s [mode] [fixture options] [test options]
    Runs a set of benchmarks and write results.

    Note: Argument order matters, latest overrides earliest.

    [Mode]
        -h,--help                      Print usage description. This message.

        -p                             Performance test mode. Default mode is
                                       Unit test.

    [Fixture Options]
        --block_device PATH            The block device exposed in PATH will be
                                       used as block device.

        --use_ramdisk                  A ramdisk will be used as block device.

        --ramdisk_block_size SIZE      Size in bytes of the ramdisk's block.

        --ramdisk_block_count COUNT    Number of blocks in the ramdisk.

        --use_fvm                      A FVM will be created on the block
                                       device.

        --fvm_slice_size SIZE          Size in bytes of the FVM's slices.

        --fs FS_NAME                   Will use FS_NAME filesystem to format
                                       the block device.
                                       (Options: blobfs, minfs)

        --seed SEED                    An unsigned integer to initialize
                                       pseudo-ramdom number generator.

    [Test Options]
         --out PATH                    In performance test mode, collected
                                       results will be written to PATH.

         --summary_path PATH           In performance test mode, result summary
                                       statistics will be written to PATH.

         --print_statistics            In performance test mode, result summary
                                       statistics will be written to STDOUT.

         --runs COUNT                  In performance test mode, limits the
                                       number of times to execute each test to
                                       COUNT.

)";

// Gather some info about the executed tests, and use it to display
// gTest lookalike summary.
struct TestStats {
  uint32_t passed = 0;
  uint32_t failed = 0;
  uint32_t skipped = 0;
  uint32_t total = 0;
};

uint64_t GetDelta(zx::ticks start) { return (fzl::TicksToNs(zx::ticks::now() - start).to_msecs()); }

void PrintTestStart(const fbl::String& name, FILE* out) {
  if (out) {
    fprintf(out, "[ RUN      ] %s\n", name.c_str());
  }
}

void PrintTestSkipped(const fbl::String& name, zx::ticks start, FILE* out) {
  if (out) {
    fprintf(out, "[  SKIPPED ] %s(%lu ms total)\n", name.c_str(), GetDelta(start));
  }
}

void PrintTestFailed(const fbl::String& name, zx::ticks start, FILE* out) {
  if (out) {
    fprintf(out, "[   FAILED ] %s(%lu ms total)\n", name.c_str(), GetDelta(start));
  }
}

void PrintTestPassed(const fbl::String& name, zx::ticks start, FILE* out) {
  if (out) {
    fprintf(out, "[   PASSED ] %s(%lu ms total)\n", name.c_str(), GetDelta(start));
  }
}

void PrintTestCaseStart(const fbl::String& name, size_t test_count, FILE* out) {
  if (out) {
    fprintf(out, "[----------] %lu tests from %s\n", test_count, name.c_str());
  }
}

void PrintTestCaseEnd(const fbl::String& name, size_t test_count, zx::ticks start, FILE* out) {
  if (out) {
    fprintf(out, "[----------] %lu tests from %s(%lu ms total)\n\n", test_count, name.c_str(),
            GetDelta(start));
  }
}

void PrintTestsCasesSummary(size_t test_case_count, const TestStats& stats, zx::ticks start,
                            FILE* out) {
  if (out) {
    fprintf(out, "[==========] %d tests from %lu test cases ran. (%lu ms total)\n", stats.total,
            test_case_count, fzl::TicksToNs(zx::ticks::now() - start).to_msecs());
    fprintf(out, "[  PASSED  ] %d tests.\n", stats.passed);
    fprintf(out, "[  FAILED  ] %d tests.\n", stats.failed);
    fprintf(out, "[  SKIPPED ] %d tests.\n", stats.skipped);
  }
}

void PrintUsage(char* arg0, FILE* out) { fprintf(out, kUsage, arg0); }

bool HasEnoughSpace(const fbl::String& block_device_path, size_t required_space) {
  if (required_space == 0) {
    return true;
  }

  fbl::unique_fd fd(open(block_device_path.c_str(), O_RDONLY));
  fzl::FdioCaller disk_caller(std::move(fd));
  fuchsia_hardware_block_BlockInfo block_info;
  zx_status_t io_status, status;
  io_status =
      fuchsia_hardware_block_BlockGetInfo(disk_caller.borrow_channel(), &status, &block_info);
  if (io_status != ZX_OK || status != ZX_OK) {
    LOG_ERROR(status, "Failed to verify block_device size.\n %s\n", block_device_path.c_str());
    return false;
  }

  if (required_space > block_info.block_count * block_info.block_size) {
    return false;
  }
  return true;
}

void RunTest(const TestInfo& test, uint32_t sample_count, bool skip, Fixture* fixture,
             perftest::ResultsSet* result_set, TestStats* stats, FILE* out) {
  zx::ticks test_start = zx::ticks::now();
  PrintTestStart(test.name, out);
  stats->total++;
  fbl::String error;
  if (skip) {
    stats->skipped++;
    PrintTestSkipped(test.name, test_start, out);
    return;
  }
  auto test_wrapper = [&test, fixture](perftest::RepeatState* state) {
    // Reset the seed before running the test.
    *fixture->mutable_seed() = fixture->options().seed;
    bool result = test.test_fn(state, fixture);
    return result;
  };

  constexpr char kTestSuite[] = "fuchsia.zircon";
  bool failed = !perftest::RunTest(kTestSuite, test.name.c_str(), test_wrapper, sample_count,
                                   result_set, &error);
  if (failed) {
    // Log if the error is from the perftest lib.
    if (!error.empty()) {
      LOG_ERROR(ZX_ERR_INTERNAL, "%s\n", error.c_str());
    }
    stats->failed++;
    PrintTestFailed(test.name, test_start, out);
    return;
  }
  PrintTestPassed(test.name, test_start, out);
  stats->passed++;
}

// Runs all tests in the given testcase.
void RunTestCase(const FixtureOptions& fixture_options,
                 const PerformanceTestOptions& performance_test_options,
                 const TestCaseInfo& test_case, perftest::ResultsSet* result_set,
                 TestStats* global_stats, FILE* out) {
  Fixture fixture(fixture_options);

  zx::ticks start = zx::ticks::now();
  PrintTestCaseStart(test_case.name, test_case.tests.size(), out);
  bool skip_tests = (fixture.SetUpTestCase() != ZX_OK);
  bool setUp = true;
  for (auto& test : test_case.tests) {
    // Verify that the disk has enough space to run the test. This is set up by the user
    // since the actual may change depending on the test input.
    skip_tests = !HasEnoughSpace(fixture.GetFsBlockDevice(), test.required_disk_space);
    if (skip_tests) {
      LOG_ERROR(ZX_ERR_NO_SPACE, "Not enough space on disk to run test.\n");
    } else if (setUp) {
      skip_tests = (fixture.SetUp() != ZX_OK);
      setUp = false;
    }
    uint32_t actual_sample_count = test_case.sample_count;
    if (actual_sample_count == 0 || performance_test_options.is_unittest) {
      actual_sample_count = performance_test_options.sample_count;
    }
    RunTest(test, actual_sample_count, skip_tests, &fixture, result_set, global_stats, out);
    if (test_case.teardown) {
      fixture.TearDown();
      setUp = true;
    }
  }
  if (!test_case.teardown) {
    fixture.TearDown();
  }
  fixture.TearDownTestCase();
  PrintTestCaseEnd(test_case.name, test_case.tests.size(), start, out);
}

}  // namespace

bool PerformanceTestOptions::IsValid(fbl::String* error) const {
  if (is_unittest) {
    return true;
  }

  // Something must be printed.
  if (result_path.empty() && summary_path.empty() && !print_statistics) {
    *error =
        "Performance test must produce output."
        " result_path(out), summary_path or prints_statistics must be set.\n";
    return false;
  }

  if (result_path == summary_path && !result_path.empty()) {
    *error = "result_path(out) and summary_path cannot point to the same file.\n";
    return false;
  }

  if (sample_count == 0) {
    *error = "sample_count must be a positive integer.\n";
    return false;
  }

  return true;
}

bool RunTestCases(const FixtureOptions& fixture_options,
                  const PerformanceTestOptions& performance_test_options,
                  const fbl::Vector<TestCaseInfo>& test_cases, FILE* out) {
  TestStats stats;
  perftest::ResultsSet result_set;
  bool error = false;
  zx::ticks start = zx::ticks::now();
  for (auto& test_case : test_cases) {
    RunTestCase(fixture_options, performance_test_options, test_case, &result_set, &stats, out);
  }
  PrintTestsCasesSummary(test_cases.size(), stats, start, out);
  if (performance_test_options.print_statistics) {
    fprintf(out, "\n");
    result_set.PrintSummaryStatistics(out);
    fprintf(out, "\n");
  }

  if (!performance_test_options.summary_path.empty()) {
    FILE* fp = fopen(performance_test_options.summary_path.c_str(), "w");
    if (fp) {
      result_set.PrintSummaryStatistics(fp);
      fclose(fp);
    } else {
      LOG_ERROR(ZX_ERR_IO, "%s\n", strerror(errno));
      error = true;
    }
  }

  if (!performance_test_options.result_path.empty()) {
    FILE* fp = fopen(performance_test_options.result_path.c_str(), "w");
    if (fp) {
      result_set.WriteJSON(fp);
      fclose(fp);
    } else {
      LOG_ERROR(ZX_ERR_IO, "%s\n", strerror(errno));
    }
  }
  return stats.failed == 0 && !error;
}

bool ParseCommandLineArgs(int argc, const char* const* argv, FixtureOptions* fixture_options,
                          PerformanceTestOptions* performance_test_options, FILE* out) {
  static const struct option opts[] = {
      {"help", no_argument, nullptr, 'h'},
      {"block_device", required_argument, nullptr, 0},
      {"use_ramdisk", no_argument, nullptr, 0},
      {"ramdisk_block_size", required_argument, nullptr, 0},
      {"ramdisk_block_count", required_argument, nullptr, 0},
      {"use_fvm", no_argument, nullptr, 0},
      {"fvm_slice_size", required_argument, nullptr, 0},
      {"fs", required_argument, nullptr, 0},
      {"out", required_argument, nullptr, 0},
      {"summary_path", required_argument, nullptr, 0},
      {"print_statistics", no_argument, nullptr, 0},
      {"runs", required_argument, nullptr, 0},
      {"seed", required_argument, nullptr, 0},
      {0, 0, 0, 0},
  };
  // Resets the internal state of getopt*, making this function idempotent.
  optind = 0;
  bool ramdisk_set = false;
  bool block_device_set = false;

  *performance_test_options = PerformanceTestOptions::UnitTest();
  // get_opt expects non const pointers.
  char** argvs = const_cast<char**>(argv);

  int c = -1;
  int option_index = -1;
  while ((c = getopt_long(argc, argvs, "ph", opts, &option_index)) >= 0) {
    switch (c) {
      case 0:
        switch (option_index) {
          case 0:
            PrintUsage(argvs[0], out);
            return false;
          case 1:
            fixture_options->block_device_path = optarg;
            block_device_set = true;
            break;
          case 2:
            fixture_options->use_ramdisk = true;
            ramdisk_set = true;
            break;
          case 3:
            fixture_options->ramdisk_block_size = atoi(optarg);
            break;
          case 4:
            fixture_options->ramdisk_block_count = atoi(optarg);
            break;
          case 5:
            fixture_options->use_fvm = true;
            break;
          case 6:
            fixture_options->fvm_slice_size = atoi(optarg);
            break;
          case 7:
            if (strcmp(optarg, "minfs") == 0) {
              fixture_options->fs_type = DISK_FORMAT_MINFS;
            } else if (strcmp(optarg, "blobfs") == 0) {
              fixture_options->fs_type = DISK_FORMAT_BLOBFS;
            } else {
              LOG_ERROR(ZX_ERR_INVALID_ARGS,
                        "Unknown disk_format %s. Support values are minfs and blobfs.\n", optarg);
              return false;
            }
            break;
          case 8:
            performance_test_options->result_path = optarg;
            break;
          case 9:
            performance_test_options->summary_path = optarg;
            break;
          case 10:
            performance_test_options->print_statistics = true;
            break;
          case 11:
            performance_test_options->sample_count = atoi(optarg);
            break;
          case 12:
            fixture_options->seed = static_cast<unsigned int>(strtoul(optarg, NULL, 0));
            break;
          default:
            break;
        }
        break;
      case 'p':
        *performance_test_options = PerformanceTestOptions::PerformanceTest();
        break;
      case 'h':
      default:
        PrintUsage(argvs[0], out);
        return false;
    }
  }

  // Unset ramdisk when not requested and block device is passed.
  if (block_device_set && !ramdisk_set) {
    fixture_options->use_ramdisk = false;
  }

  bool ok = true;
  fbl::String error;
  if (!fixture_options->IsValid(&error)) {
    LOG_ERROR(ZX_ERR_INVALID_ARGS, "%s\n", error.c_str());
    ok = false;
  }

  if (!performance_test_options->IsValid(&error)) {
    LOG_ERROR(ZX_ERR_INVALID_ARGS, "%s\n", error.c_str());
    ok = false;
  }

  if (!ok) {
    PrintUsage(argvs[0], out);
  }

  return ok;
}

}  // namespace fs_test_utils
